iT邦幫忙

2021 iThome 鐵人賽

DAY 19
0
Modern Web

從零開始打造網頁遊戲-造輪子你也辦的到!系列 第 19

Chapter4 用音樂做動畫 結合前三章學習的內容,一口氣衝刺吧!

  • 分享至 

  • xImage
  •  

題外話

昨天沒把樹葉畫上去,還是心癢癢的,所以動手簡單裝飾了一下這棵樹:
https://jerry-the-potato.github.io/Chapter3-demo2-object/
不過靈感跟美感畢竟不太夠,還是趕快前進到第四章要緊先
(進度快的話最後還能把樹放到遊戲中然後優化它)

差點忘了我們要做網頁遊戲!?

是時候結合1-3章的內容了,首先我們觀察一下幾首歌的音量變化:(音量增加圖)

出自: 我的Demo
https://ithelp.ithome.com.tw/upload/images/20210926/201351976xXsT9FmZg.png
https://ithelp.ithome.com.tw/upload/images/20210926/20135197m7NBFNGKP6.png

當初沒有教大家怎麼畫這個,沒關係這不難也不是重點,聽結論就好

經過觀察得知,每當音符/和弦出來的時候,都會有2-4偵的音量急遽增加,隨後緩慢降低,因此第一個思路就很單純,我們希望程式能懂得把連續的音量脈衝,當成完整一次的旋律,並且在第2偵時會接近高峰值,也是音樂正在傳入耳朵時,因此將其脈衝大小作為參考,來決定要一次做出多少個動畫。

接著看下一首,有鋼琴和弦作為開場的音樂:
https://ithelp.ithome.com.tw/upload/images/20210926/20135197UvILWT4uVc.png

可以看到,音量的曲線應該接近於Ease-out達到高峰,所以音量的增加才會如圖,到達高峰後陸續下降並趨於平緩,這種和弦帶來的效果是拉長的聽覺效果,先做個筆記,未來有時間可以再新增第二種動畫模式,來呈現其綿延的旋律。

判讀音量的狀態

剛也有提到音量增加圖表示了音量的差值,直接取名為volume,還記得我們用reduce方法的那篇寫到:

// 陣列最大長度到 INDEX 為止
(dataIndex.volume > INDEX) ? dataIndex.volume = 0 : dataIndex.volume++;
let volume = dataArray.delta.reduce((a,b) => a+b, 0);
dataArray.volume.splice(dataIndex.volume, 1, volume);
// 計算最大值
let maxVolume = dataArray.volume.reduce((a,b) => Math.max(a,b), 1);

沒印象的話可以回頭去看這篇 https://ithelp.ithome.com.tw/articles/10268606

當初為了畫圖已經有做好這個陣列,這邊我們就有很簡單的方法,拿到最近三次的差值:

let v1 = dataArray.volume[dataIndex.volume - 1];
let v2 = dataArray.volume[dataIndex.volume - 2];
let v3 = dataArray.volume[dataIndex.volume - 3];
// 我們只要連續兩次,連續三次以上都不算數,因此判斷 v3 <= 0
if(v1 > 0 && v2 > 0 && v3 <= 0){
    let times = 100 * Math.max(v1, v2) / maxVolume; // 100乘上一個0~1之間的數
    animeList.push(new animeObject(times, 'Falling'));
}

animeObject 是我們第二章操作的落葉物件
animeList 是個把物件們放在一起,用來迭代的陣列

動畫物件(建構式)

let animeList = new Array();
function animeObject(times, animeName='Falling',
                     imgNumber=Math.floor(Math.random()*4),
                     sizeMin=0.03,sizeMax=0.04,
                     lifeTime=5, timestamp=Date.now()){
    this.animeName = animeName;
    this.imgNumber = imgNumber;
    this.img = pngImg[this.imgNumber];
    if(animeName == "Falling" || animeName == "Staring"){
        this.beginX = Math.random() * WIDTH;
        this.beginY = Math.random() * HEIGHT;
    }
    this.size = Math.random() * (sizeMax - sizeMin) + sizeMin;
    this.timestamp = Date.now();
    this.lifeTime = lifeTime;
    this.period = 1 + Math.random() * 1;

    // 變化屬性
    this.pointX = this.beginX;
    this.pointY = this.beginY;
    this.sizeNow = 0;
    this.rotateTheta = Math.random() * 360 / 180 * Math.PI;
    this.rotateOmega = 60 / 180 * Math.PI;
    this.revolveTheta = Math.random() * 360 / 180 * Math.PI;
    this.revolveOmega = 60 / 180 * Math.PI;
    if(times > 5) animeList.push(new animeObject(Math.pow(times, 0.9), 'Falling'));

    // times 的算法可以自行設計,就是把輸入的參數轉換成迭代的參數
    // 我是設計為5-100之間會進行迭代,然後每次開0.9次方根號
    // (100共會做十次、40會做八次、20會做六次、12會做四次、7會做兩次)
}

開頭傳入參數時,若無值傳入,則預設為Falling模式、圖片以4個一組隨機抽籤etc...
整體跟落葉物件那篇相去不遠,主要添加了遞迴的觀念
以及一個隨機的週期參數period,使動畫物件彼此擁有不同的週期,範圍是1~2倍

動畫方法

接著搭配第三章畫樹時學到的prototype改寫方法:

animeObject.prototype.NextFrame = function(){
    
    // 計算下一偵的位置
    let dT = (Date.now() - this.timestamp) / 1000;
    if(this.animeName == "Floating") this.Floating(dT);
    else if(this.animeName == "Falling") this.Falling(dT);
    else if(this.animeName == "Staring") this.Staring(dT);

    if(dT < this.lifeTime){
        // 畫出下一偵的位置
        if(this.img.complete){
            let width = this.sizeNow;
            let height = this.sizeNow * this.img.height / this.img.width;
            let rotateNow = this.rotateTheta + this.rotateOmega * dT;
            context.save();
            context.translate(this.pointX, this.pointY);
            context.rotate(rotateNow);
            context.drawImage(this.img, -width/2, -height/2, width, height);
            context.restore();
        }
    }
    else{
        // 把動畫物件刪掉
        let index = animeList.indexOf(this);
        delete animeList[index];
        animeList.splice(index, 1);
    }
}

計算下一偵的位置

計算下一偵的位置,根據不同種類的動畫公式去做計算,以落下為例子:

animeObject.prototype.Falling = function(dT){
    let revolveNow = this.revolveTheta + this.revolveOmega * dT;
    let A = Math.sin(revolveNow);
    let B = Math.cos(revolveNow);
    let C = Math.sin(revolveNow * this.period);
    let D = Math.cos(revolveNow * this.period);
    this.pointX = this.beginX + WIDTH * 0.04 * A;
    this.pointY = this.beginY + HEIGHT * 0.01 * C + HEIGHT * 0.05 * dT;
    const popSize = 0.2;
    let lT = this.lifeTime;
    let distanceT = (Math.abs(dT - lT*0.35) + Math.abs(dT - lT*0.65)) / lT;
    this.sizeNow = WIDTH * this.size * (popSize + (1 - distanceT));
}

在第二章的動畫基礎上,添加幾個部分:

  1. 有兩種三角函數,一種是基本的、另一種是乘上週期倍速period的變速版本
  2. 添加popSize,使得動畫一開始出現時以只有0.2倍大(從遠方漸入的效果)
  3. distanceT是一個國中學過的線性函式,用來計算數線0-1之間,任一點與0.35和0.65的距離總和,結果就不用解釋...了吧?XD

後記

接下來第四章(包括這篇)都是重頭戲,先前鋪陳那麼久,就是為了能讓大家循序漸進好上手,如果前面的章節你都看過,想必這一篇並不會太難吧!也是不斷精簡過後的內容了,這段code是我重寫第三次了,第一次大概是2倍的行數,第二次是1.5倍,現在就如各位看到的。

話說,明天上個流程圖吧!


上一篇
Chpater3 今天來學習畫一棵樹(IV)淺談效能和演算法,以迭代取代遞迴吧!
下一篇
Chapter4 - Canvas背景動畫(I)讓落葉隨風飄落、自然搖擺
系列文
從零開始打造網頁遊戲-造輪子你也辦的到!31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言